全球化的網站不免都要做多國語言,ASP.NET Core 的多國語言設定方式跟 ASP.NET MVC 有很大的落差。
本篇將介紹 ASP.NET Core 多國語言 (Localization) 的設定方式。
同步發佈至個人部落格:
John Wu's Blog - [鐵人賽 Day21] ASP.NET Core 2 系列 - 多國語言 (Localization)
過去 ASP.NET 語系檔都是用 *.resx 格式,現在 ASP.NET Core 也是沿用此格式,但檔案結構確很不一樣。ASP.NET Core 語系檔命名規則必須要與類別的 namespace 階層相互對應。例如 Controllers、Views、Models 要用的語系檔跟類別對應如下:
多國語言檔建立規則跟 ASP.NET MVC 有很大的差別。
*.resx 檔案必須對應使用的路徑位置。*.resx 檔案的語系帶在後綴。如:*.en-GB.resx。*.resx 語系檔內容大致如下:
Resources\Controllers.HomeController.en-GB.resx
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="Hello">
    <value>Hello~ This message from Resources\Controllers.HomeController.en-GB.resx</value>
  </data>
</root>
若以 Visual Studio IDE 開發 (如 Visual Studio 2017),可以從 UI 新增資源檔 *.resx。在網站目錄中建立 Resources 的資料夾,並新增資源檔 *.resx。如下:
![[鐵人賽 Day21] ASP.NET Core 2 系列 - 多國語言 (Localization) - 新增資源檔 1](https://blog.johnwu.cc/images/pasted-200.png)
![[鐵人賽 Day21] ASP.NET Core 2 系列 - 多國語言 (Localization) - 新增資源檔 2](https://blog.johnwu.cc/images/pasted-201.png)
ASP.NET Core 使用多國語言,需要 Microsoft.AspNetCore.Localization 套件。
在此範例中我還需要從 Routing 抓取語系的資訊,所以也需要 Microsoft.AspNetCore.Localization.Routing 套件。
透過 .NET Core CLI 在專案資料夾執行安裝指令:
dotnet add package Microsoft.AspNetCore.Localization
dotnet add package Microsoft.AspNetCore.Routing
ASP.NET Core 2.0 以上版本,預設是參考
Microsoft.AspNetCore.All,已經包含Microsoft.AspNetCore.Localization及Microsoft.AspNetCore.Routing,所以不用再安裝。
在 Startup.ConfigureServices 註冊多國語言需要的服務,以及修改多國語的 Routing 方式。如下:
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection;
namespace MyWebsite
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddLocalization(options => options.ResourcesPath = "Resources");
            services.AddMvc()
                    .AddViewLocalization()
                    .AddDataAnnotationsLocalization();
        }
        public void Configure(IApplicationBuilder app)
        {
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{culture=en-GB}/{controller=Home}/{action=Index}/{id?}"
                );
            });
        }
    }
}
如果不想用 Routing 的方式,也可以改用 QueryString 帶入語系資訊。
建立一個 CultureMiddleware 來包裝 Localization 的 Middleware,可以做支援語言的管理。
Middlewares\CultureMiddleware.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Localization.Routing;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace MyWebsite.Middlewares
{
    public class CultureMiddleware
    {
        private static readonly List<CultureInfo> _supportedCultures = new List<CultureInfo>
        {
            new CultureInfo("en-GB"),
            new CultureInfo("zh-TW")
        };
        private static readonly RequestLocalizationOptions _localizationOptions = new RequestLocalizationOptions()
        {
            DefaultRequestCulture = new RequestCulture(_supportedCultures.First()),
            SupportedCultures = _supportedCultures,
            SupportedUICultures = _supportedCultures,
            RequestCultureProviders = new[]
            {
                new RouteDataRequestCultureProvider() { Options = _localizationOptions }
            }
        };
        public void Configure(IApplicationBuilder app)
        {
            app.UseRequestLocalization(_localizationOptions);
        }
    }
}
每個 Requset 都會執行 RequestCultureProviders 中的 CultureProvider,用來判斷語系資訊,套用正確的資源檔。
Microsoft.AspNetCore.Localization 套件支援的 CultureProvider 有三種:
http://localhost:500/?culture=zh-TW
而我是用 Routing 判斷語系資訊,以上三種都不合我用。
Routing 判斷語系可以用 Microsoft.AspNetCore.Localization.Routing 套件的 RouteDataRequestCultureProvider。
把 CultureMiddleware 註冊在需要用到的 Controller 或 Action。如下:
Controllers\HomeController.cs
[MiddlewareFilter(typeof(CultureMiddleware))]
public class HomeController : Controller
{
    // ...
}
通常 ASP.NET Core 網站會伴隨著 API,API 不需要語系資訊,所以不建議註冊在全域。
在 Controller 要使用多國語言的話,需要在建構子加入 IStringLocalizer 參數,執行期間會把 _localizer 的實體注入近來。
把 Resource Key 丟入 _localizer,就可以得到該語系的值。
Controllers\HomeController.cs
using System.Globalization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using MyWebsite.Middlewares;
namespace MyWebsite
{
    [MiddlewareFilter(typeof(CultureMiddleware))]
    public class HomeController : Controller
    {
        private readonly IStringLocalizer _localizer;
        public HomeController(IStringLocalizer<HomeController> localizer)
        {
            _localizer = localizer;
        }
        public IActionResult Index()
        {
            return View();
        }
        public IActionResult Content()
        {
            return Content($"CurrentCulture: {CultureInfo.CurrentCulture.Name}\r\n"
                         + $"CurrentUICulture: {CultureInfo.CurrentUICulture.Name}\r\n"
                         + $"{_localizer["Hello"]}");
        }
    }
}
要在 cshtml 使用多國語言的話,要先在 Services 中加入 ViewLocalization。
注入 IViewLocalizer,同上把 Resource Key 丟入 Localizer,就可以得到值。
Views\Home\Index.cshtml
@using System.Globalization
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer localizer
CurrentCulture: @CultureInfo.CurrentCulture.Name <br />
CurrentUICulture: @CultureInfo.CurrentUICulture.Name <br />
@localizer["Hello"]<br />
要在 Model 使用多國語言的話,要先在 Services 中加入 DataAnnotationsLocalization。
Models\SampleModel.cs
using System.ComponentModel.DataAnnotations;
namespace MyWebsite.Models
{
    public class SampleModel
    {
        [Display(Name = "Hello")]
        public string Content { get; set; }
    }
}
Controllers\HomeController.cs
// ...
[MiddlewareFilter(typeof(CultureMiddleware))]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View(model: new SampleModel());
    }
}
Views\Home\Index.cshtml
@using System.Globalization
@using MyWebsite.Models
@model SampleModel
CurrentCulture: @CultureInfo.CurrentCulture.Name <br />
CurrentUICulture: @CultureInfo.CurrentUICulture.Name <br />
@Html.DisplayNameFor(m => m.Content)<br />
![[鐵人賽 Day21] ASP.NET Core 2 系列 - 多國語言 (Localization) - 範例執行結果](https://blog.johnwu.cc/images/i21-1.png)
ASP.NET Core 語系檔命名規則為了與 Controllers、Views、Models 相互對應,可能會產生一大堆檔案,造成維護上的困擾。
因此,可以利用 ASP.NET Core DI 的特性,建立一個共用的語系檔,再將該語系資訊注入至 DI 容器。
建立共用的語系檔 Resources\SharedResource.en-GB.resx,同時建立一個對應的 SharedResource.cs 檔案,內容如下:
Resources\SharedResource.en-GB.resx
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="Hello">
    <value>Hello~ This message from Resources\SharedResource.en-GB.resx</value>
  </data>
</root>
SharedResource.cs
using Microsoft.Extensions.Localization;
namespace MyWebsite
{
    public class SharedResource
    {
        private readonly IStringLocalizer _localizer;
        public SharedResource(IStringLocalizer<SharedResource> localizer)
        {
            _localizer = localizer;
        }
    }
}
IStringLocalizer 注入的型別改成 SharedResource,如下:
Controllers\HomeController.cs
using System.Globalization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using MyWebsite.Middlewares;
using MyWebsite.Models;
namespace MyWebsite
{
    [MiddlewareFilter(typeof(CultureMiddleware))]
    public class HomeController : Controller
    {
        private readonly IStringLocalizer _localizer;
        private readonly IStringLocalizer _sharedLocalizer;
        public HomeController(IStringLocalizer<HomeController> localizer,
            IStringLocalizer<SharedResource> sharedLocalizer)
        {
            _localizer = localizer;
            _sharedLocalizer = sharedLocalizer;
        }
        public IActionResult Index()
        {
            return View(model: new SampleModel());
        }
        public string Content()
        {
            return $"CurrentCulture: {CultureInfo.CurrentCulture.Name}\r\n"
                 + $"CurrentUICulture: {CultureInfo.CurrentUICulture.Name}\r\n"
                 + $"{_localizer["Hello"]}\r\n"
                 + $"{_sharedLocalizer["Hello"]}";
        }
    }
}
注入 IViewLocalizer 改成注入 IHtmlLocalizer,並指派型別,如下:
Views\Home\Index.cshtml
@using System.Globalization
@using Microsoft.AspNetCore.Mvc.Localization
@using MyWebsite.Models
@model SampleModel
@inject IViewLocalizer localizer
@inject IHtmlLocalizer<MyWebsite.SharedResource> sharedLocalizer
CurrentCulture: @CultureInfo.CurrentCulture.Name <br />
CurrentUICulture: @CultureInfo.CurrentUICulture.Name <br />
@localizer["Hello"]<br />
@Html.DisplayNameFor(m => m.Content)<br />
@sharedLocalizer["Hello"]<br />
![[鐵人賽 Day21] ASP.NET Core 2 系列 - 多國語言 (Localization) - 共用語系檔範例執行結果](https://blog.johnwu.cc/images/i21-2.png)
ASP.NET Core Globalization and localization